Hrvatski

Dubinski uvid u Reactov useReducer hook za učinkovito upravljanje složenim stanjima aplikacije, čime se poboljšavaju performanse i održivost globalnih React projekata.

React useReducer uzorak: Ovladavanje upravljanjem složenim stanjem

U svijetu front-end razvoja koji se neprestano razvija, React se etablirao kao vodeći framework za izradu korisničkih sučelja. Kako aplikacije postaju složenije, upravljanje stanjem postaje sve veći izazov. Hook useState pruža jednostavan način za upravljanje stanjem unutar komponente, ali za složenije scenarije, React nudi moćnu alternativu: hook useReducer. Ovaj blog post duboko ulazi u useReducer uzorak, istražujući njegove prednosti, praktične implementacije i kako može značajno poboljšati vaše React aplikacije na globalnoj razini.

Razumijevanje potrebe za upravljanjem složenim stanjem

Pri izradi React aplikacija često se susrećemo sa situacijama u kojima stanje komponente nije samo jednostavna vrijednost, već skup međusobno povezanih podataka ili stanje koje ovisi o prethodnim vrijednostima stanja. Razmotrite ove primjere:

U ovim scenarijima, korištenje samo useState hooka može dovesti do složenog koda kojim je teško upravljati. Može postati nezgrapno ažurirati više varijabli stanja kao odgovor na jedan događaj, a logika za upravljanje tim ažuriranjima može postati raspršena po komponenti, što otežava razumijevanje i održavanje. Ovdje useReducer dolazi do izražaja.

Predstavljanje useReducer hooka

Hook useReducer je alternativa useState hooku za upravljanje složenom logikom stanja. Temelji se na principima Redux uzorka, ali je implementiran unutar same React komponente, čime se u mnogim slučajevima eliminira potreba za zasebnom vanjskom bibliotekom. Omogućuje vam centraliziranje logike ažuriranja stanja u jednoj funkciji koja se naziva reducer.

Hook useReducer prima dva argumenta:

Hook vraća niz koji sadrži dva elementa:

Reducer funkcija

Reducer funkcija je srce useReducer uzorka. To je čista funkcija, što znači da ne bi trebala imati nikakve nuspojave (poput API poziva ili modificiranja globalnih varijabli) i uvijek bi trebala vraćati isti izlaz za isti ulaz. Reducer funkcija prima dva argumenta:

Unutar reducer funkcije koristite switch naredbu ili if/else if naredbe za obradu različitih vrsta akcija i odgovarajuće ažuriranje stanja. To centralizira vašu logiku ažuriranja stanja i olakšava razumijevanje kako se stanje mijenja kao odgovor na različite događaje.

Dispatch funkcija

Dispatch funkcija je metoda koju koristite za pokretanje ažuriranja stanja. Kada pozovete dispatch(action), akcija se prosljeđuje reducer funkciji, koja zatim ažurira stanje na temelju vrste i payload-a akcije.

Praktičan primjer: Implementacija brojača

Krenimo s jednostavnim primjerom: komponenta brojača. Ovo ilustrira osnovne koncepte prije nego što prijeđemo na složenije primjere. Napravit ćemo brojač koji se može povećavati, smanjivati i resetirati:


import React, { useReducer } from 'react';

// Definiranje vrsta akcija
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';

// Definiranje reducer funkcije
function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    case RESET:
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  // Inicijalizacija useReducer-a
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Broj: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>Povećaj</button>
      <button onClick={() => dispatch({ type: DECREMENT })}>Smanji</button>
      <button onClick={() => dispatch({ type: RESET })}>Resetiraj</button>
    </div>
  );
}

export default Counter;

U ovom primjeru:

Proširenje primjera brojača: Dodavanje payload-a

Izmijenimo brojač kako bismo omogućili povećanje za određenu vrijednost. Ovo uvodi koncept payload-a u akciji:


import React, { useReducer } from 'react';

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_VALUE = 'SET_VALUE';

function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + action.payload };
    case DECREMENT:
      return { count: state.count - action.payload };
    case RESET:
      return { count: 0 };
    case SET_VALUE:
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  const [inputValue, setInputValue] = React.useState(1);

  return (
    <div>
      <p>Broj: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT, payload: parseInt(inputValue) || 1 })}>Povećaj za {inputValue}</button>
      <button onClick={() => dispatch({ type: DECREMENT, payload: parseInt(inputValue) || 1 })}>Smanji za {inputValue}</button>
      <button onClick={() => dispatch({ type: RESET })}>Resetiraj</button>
       <input
         type="number"
         value={inputValue}
         onChange={(e) => setInputValue(e.target.value)}
       />
      </div>
  );
}

export default Counter;

U ovom proširenom primjeru:

Prednosti korištenja useReducer-a

Uzorak useReducer nudi nekoliko prednosti u odnosu na izravno korištenje useState-a za upravljanje složenim stanjem:

Kada koristiti useReducer

Iako useReducer nudi značajne prednosti, nije uvijek pravi izbor. Razmislite o korištenju useReducer-a kada:

Za jednostavna ažuriranja stanja, useState je često dovoljan i jednostavniji za korištenje. Prilikom donošenja odluke uzmite u obzir složenost vašeg stanja i potencijal za rast.

Napredni koncepti i tehnike

Kombiniranje useReducer-a s Context API-jem

Za upravljanje globalnim stanjem ili dijeljenje stanja između više komponenti, možete kombinirati useReducer s Reactovim Context API-jem. Ovaj pristup se često preferira u odnosu na Redux za manje do srednje velike projekte gdje ne želite uvoditi dodatne ovisnosti.


import React, { createContext, useReducer, useContext } from 'react';

// Definiranje vrsta akcija i reducera (kao i prije)
const INCREMENT = 'INCREMENT';
// ... (druge vrste akcija i counterReducer funkcija)

const CounterContext = createContext();

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

function useCounter() {
  return useContext(CounterContext);
}

function Counter() {
  const { state, dispatch } = useCounter();

  return (
    <div>
      <p>Broj: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>Povećaj</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

export default App;

U ovom primjeru:

Testiranje useReducer-a

Testiranje reducera je jednostavno jer su to čiste funkcije. Možete lako testirati reducer funkciju izolirano pomoću okvira za jedinično testiranje kao što su Jest ili Mocha. Evo primjera korištenja Jesta:


import { counterReducer } from './counterReducer'; // Pretpostavljajući da je counterReducer u zasebnoj datoteci

const INCREMENT = 'INCREMENT';

describe('counterReducer', () => {
  it('trebao bi povećati brojač', () => {
    const state = { count: 0 };
    const action = { type: INCREMENT };
    const newState = counterReducer(state, action);
    expect(newState.count).toBe(1);
  });

   it('trebao bi vratiti isto stanje za nepoznate vrste akcija', () => {
        const state = { count: 10 };
        const action = { type: 'UNKNOWN_ACTION' };
        const newState = counterReducer(state, action);
        expect(newState).toBe(state); // Provjera da se stanje nije promijenilo
    });
});

Testiranje vaših reducera osigurava da se ponašaju kako se očekuje i olakšava refaktoriranje logike stanja. Ovo je ključan korak u izgradnji robusnih i održivih aplikacija.

Optimizacija performansi pomoću memoizacije

Kada radite sa složenim stanjima i čestim ažuriranjima, razmislite o korištenju useMemo za optimizaciju performansi vaših komponenti, posebno ako imate izvedene vrijednosti izračunate na temelju stanja. Na primjer:


import React, { useReducer, useMemo } from 'react';

function reducer(state, action) {
  // ... (logika reducera) 
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  // Izračunaj izvedenu vrijednost, memoizirajući je s useMemo
  const derivedValue = useMemo(() => {
    // Računski zahtjevan izračun temeljen na stanju
    return state.value1 + state.value2;
  }, [state.value1, state.value2]); // Ovisnosti: ponovno izračunaj samo kada se ove vrijednosti promijene

  return (
    <div>
      <p>Izvedena vrijednost: {derivedValue}</p>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE1', payload: 10 })}>Ažuriraj vrijednost 1</button>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE2', payload: 20 })}>Ažuriraj vrijednost 2</button>
    </div>
  );
}

U ovom primjeru, derivedValue se izračunava samo kada se state.value1 ili state.value2 promijene, sprječavajući nepotrebne izračune pri svakom ponovnom renderiranju. Ovaj pristup je uobičajena praksa za osiguravanje optimalnih performansi renderiranja.

Primjeri iz stvarnog svijeta i slučajevi upotrebe

Istražimo nekoliko praktičnih primjera gdje je useReducer vrijedan alat u izradi React aplikacija za globalnu publiku. Imajte na umu da su ovi primjeri pojednostavljeni kako bi ilustrirali osnovne koncepte. Stvarne implementacije mogu uključivati složeniju logiku i ovisnosti.

1. Filteri proizvoda u e-trgovini

Zamislite web stranicu za e-trgovinu (poput popularnih platformi kao što su Amazon ili AliExpress, dostupnih globalno) s velikim katalogom proizvoda. Korisnici trebaju filtrirati proizvode po različitim kriterijima (raspon cijena, marka, veličina, boja, zemlja podrijetla itd.). useReducer je idealan za upravljanje stanjem filtera.


import React, { useReducer } from 'react';

const initialState = {
  priceRange: { min: 0, max: 1000 },
  brand: [], // Niz odabranih marki
  color: [], // Niz odabranih boja
  //... ostali kriteriji filtriranja
};

function filterReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_PRICE_RANGE':
      return { ...state, priceRange: action.payload };
    case 'TOGGLE_BRAND':
      const brand = action.payload;
      return { ...state, brand: state.brand.includes(brand) ? state.brand.filter(b => b !== brand) : [...state.brand, brand] };
    case 'TOGGLE_COLOR':
      // Slična logika za filtriranje boja
      return { ...state, color: state.color.includes(action.payload) ? state.color.filter(c => c !== action.payload) : [...state.color, action.payload] };
    // ... ostale akcije filtriranja
    default:
      return state;
  }
}

function ProductFilter() {
  const [state, dispatch] = useReducer(filterReducer, initialState);

  // UI komponente za odabir kriterija filtriranja i pokretanje dispatch akcija
  // Na primjer: klizač za cijenu, potvrdni okviri za marke, itd.

  return (
    <div>
      <!-- UI elementi filtera -->
    </div>
  );
}

Ovaj primjer pokazuje kako upravljati s više kriterija filtriranja na kontroliran način. Kada korisnik izmijeni bilo koju postavku filtera (cijenu, marku itd.), reducer ažurira stanje filtera u skladu s tim. Komponenta odgovorna za prikaz proizvoda zatim koristi ažurirano stanje za filtriranje prikazanih proizvoda. Ovaj uzorak podržava izgradnju složenih sustava filtriranja uobičajenih na globalnim platformama za e-trgovinu.

2. Višekoračni obrasci (npr. obrasci za međunarodnu dostavu)

Mnoge aplikacije uključuju višekoračne obrasce, poput onih koji se koriste za međunarodnu dostavu ili stvaranje korisničkih računa sa složenim zahtjevima. useReducer se ističe u upravljanju stanjem takvih obrazaca.


import React, { useReducer } from 'react';

const initialState = {
  step: 1, // Trenutni korak u obrascu
  formData: {
    firstName: '',
    lastName: '',
    address: '',
    city: '',
    country: '',
    // ... ostala polja obrasca
  },
  errors: {},
};

function formReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    case 'PREV_STEP':
      return { ...state, step: state.step - 1 };
    case 'UPDATE_FIELD':
      return { ...state, formData: { ...state.formData, [action.payload.field]: action.payload.value } };
    case 'SET_ERRORS':
      return { ...state, errors: action.payload };
    case 'SUBMIT_FORM':
      // Ovdje obradite logiku podnošenja obrasca, npr. API pozive
      return state;
    default:
      return state;
  }
}

function MultiStepForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  // Logika renderiranja za svaki korak obrasca
  // Na temelju trenutnog koraka u stanju
  const renderStep = () => {
    switch (state.step) {
      case 1:
        return <Step1 formData={state.formData} dispatch={dispatch} />;
      case 2:
        return <Step2 formData={state.formData} dispatch={dispatch} />;
      // ... ostali koraci
      default:
        return <p>Nevažeći korak</p>;
    }
  };

  return (
    <div>
      {renderStep()}
      <!-- Navigacijski gumbi (Dalje, Prethodno, Pošalji) na temelju trenutnog koraka -->
    </div>
  );
}

Ovo ilustrira kako upravljati različitim poljima obrasca, koracima i potencijalnim pogreškama pri validaciji na strukturiran i održiv način. Ključno je za izgradnju korisnički prilagođenih procesa registracije ili naplate, posebno za međunarodne korisnike koji mogu imati različita očekivanja na temelju svojih lokalnih običaja i iskustva s raznim platformama poput Facebooka ili WeChata.

3. Aplikacije u stvarnom vremenu (Chat, alati za suradnju)

useReducer je koristan za aplikacije u stvarnom vremenu, kao što su alati za suradnju poput Google Docsa ili aplikacije za razmjenu poruka. Obrađuje događaje poput primanja poruka, pridruživanja/odlaska korisnika i statusa veze, osiguravajući da se korisničko sučelje ažurira prema potrebi.


import React, { useReducer, useEffect } from 'react';

const initialState = {
  messages: [],
  users: [],
  connectionStatus: 'connecting',
};

function chatReducer(state, action) {
  switch (action.type) {
    case 'RECEIVE_MESSAGE':
      return { ...state, messages: [...state.messages, action.payload] };
    case 'USER_JOINED':
      return { ...state, users: [...state.users, action.payload] };
    case 'USER_LEFT':
      return { ...state, users: state.users.filter(user => user.id !== action.payload.id) };
    case 'SET_CONNECTION_STATUS':
      return { ...state, connectionStatus: action.payload };
    default:
      return state;
  }
}

function ChatRoom() {
  const [state, dispatch] = useReducer(chatReducer, initialState);

  useEffect(() => {
    // Uspostavljanje WebSocket veze (primjer):
    const socket = new WebSocket('wss://your-websocket-server.com');

    socket.onopen = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connected' });
    socket.onmessage = (event) => dispatch({ type: 'RECEIVE_MESSAGE', payload: JSON.parse(event.data) });
    socket.onclose = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' });

    return () => socket.close(); // Čišćenje prilikom demontiranja komponente
  }, []);

  // Renderiranje poruka, popisa korisnika i statusa veze na temelju stanja
  return (
    <div>
      <p>Status veze: {state.connectionStatus}</p>
      <!-- UI za prikazivanje poruka, popisa korisnika i slanje poruka -->
    </div>
  );
}

Ovaj primjer pruža osnovu za upravljanje chatom u stvarnom vremenu. Stanje upravlja pohranom poruka, korisnicima koji su trenutno u chatu i statusom veze. Hook useEffect odgovoran je za uspostavljanje WebSocket veze i obradu dolaznih poruka. Ovakav pristup stvara responzivno i dinamično korisničko sučelje koje je prilagođeno korisnicima diljem svijeta.

Najbolje prakse za korištenje useReducer-a

Kako biste učinkovito koristili useReducer i stvarali održive aplikacije, razmotrite ove najbolje prakse:

Zaključak

Hook useReducer je moćan i svestran alat za upravljanje složenim stanjem u React aplikacijama. Nudi brojne prednosti, uključujući centraliziranu logiku stanja, poboljšanu organizaciju koda i poboljšanu mogućnost testiranja. Slijedeći najbolje prakse i razumijevajući njegove osnovne koncepte, možete iskoristiti useReducer za izgradnju robusnijih, održivijih i učinkovitijih React aplikacija. Ovaj uzorak vas osnažuje da se učinkovito nosite s izazovima upravljanja složenim stanjem, omogućujući vam izgradnju aplikacija spremnih za globalno tržište koje pružaju besprijekorno korisničko iskustvo diljem svijeta.

Kako dublje ulazite u razvoj s Reactom, uključivanje useReducer uzorka u vaš set alata nedvojbeno će dovesti do čišćih, skalabilnijih i lakše održivih kodnih baza. Zapamtite da uvijek uzimate u obzir specifične potrebe vaše aplikacije i odabirete najbolji pristup upravljanju stanjem za svaku situaciju. Sretno kodiranje!